Перейти к основному содержимому

5.15. Метатаблицы и метаметоды

Разработчику Архитектору

Метатаблицы и метаметоды

Поведение таблиц
Переопределение поведения таблиц
Метатаблица
Метаметод
__add, __index, __newindex, __call, __tostring и другие примеры
реализация классов через __index
Аналог полиморфизма, операторной перегрузки.
ООП на основе таблиц и метатаблиц
Нет классов «из коробки» — но можно эмулировать.
Наследование через цепочки метатаблиц.
Сравнение с Ruby/Python: гибкость vs читаемость.

Таблицы используются повсеместно: как массивы, словари, объекты, модули и даже классы. Однако их истинная гибкость проявляется не только в структуре хранения данных, но и в возможности динамического переопределения поведения операций над ними. Именно эту возможность обеспечивают метатаблицы и метаметоды.

Таблица в Lua — это ассоциативный массив, реализованный как хеш-таблица (или комбинация массива и хеша для оптимизации). Она представляет собой коллекцию пар ключ–значение, где ключи и значения могут быть любыми типами, кроме nil. В памяти таблица организована как динамическая структура, поддерживающая эффективные операции вставки, поиска и удаления.

local t = { x = 10, y = 20 }

Это эквивалентно:

local t = {}
t.x = 10
t.y = 20

Внутри интерпретатора каждая таблица содержит:

  • Указатель на массив компонентов (для числовых индексов, начиная с 1),
  • Хеш-таблицу для произвольных ключей,
  • Ссылку на метатаблицу (если она установлена).

Таблицы — единственный составной тип в Lua, способный к изменению поведения через внешние механизмы. Это достигается за счёт метатаблиц.

Метатаблица (metatable) — это обычная таблица, присоединённая к другой таблице (или userdata), которая определяет специальное поведение последней при выполнении определённых операций.

Каждая таблица может иметь не более одной метатаблицы. Метатаблица не наследуется автоматически; она устанавливается явно с помощью функции setmetatable или возвращается функцией getmetatable.

local t = {}
local mt = {}
setmetatable(t, mt)
assert(getmetatable(t) == mt)

Метатаблица действует как описатель поведения: она не хранит данные объекта напрямую, но задаёт правила, по которым объект реагирует на операции, такие как сложение, доступ к полям, вызов как функции и т.д.

Метаметод (metamethod) — это поле внутри метатаблицы, имя которого начинается с двойного подчёркивания (__), и которое определяет поведение при определённой операции. Например, метаметод __add определяет, что происходит при использовании оператора + с таблицей.

Каждый метаметод соответствует конкретному событию в жизненном цикле операции. Когда интерпретатор сталкивается с операцией над таблицей, он проверяет наличие соответствующего метаметода в её метатаблице и, если он найден, вызывает его вместо стандартного поведения.

При попытке выполнить операцию a + b, если один из операндов — таблица с метатаблицей, содержащей __add, вызывается именно эта функция. Если нет — генерируется ошибка (если оба операнда не являются числами).

Рассмотрим наиболее важные метаметоды и их применение.

  1. __index — перехват чтения отсутствующих полей. Определяет поведение при попытке доступа к несуществующему ключу.
    • Если __index — функция, она вызывается с двумя аргументами: таблицей и ключом.
    • Если __index — таблица, поиск продолжается в этой таблице.
local defaults = { color = "white", size = "medium" }
local obj = {}
setmetatable(obj, { __index = defaults })

print(obj.color) -- "white" (берётся из defaults)

Этот механизм лежит в основе делегирования и используется для эмуляции наследования. 2. __newindex — перехват записи в поля. Вызывается при попытке присвоить значение новому (или существующему) ключу.

  • Позволяет контролировать, как и куда записываются данные.
  • Может использоваться для создания защищённых таблиц, прокси или реактивных систем.
local t = {}
local proxy = {}
setmetatable(proxy, {
__newindex = function(tbl, key, value)
print("Запись:", key, "=", value)
rawset(t, key, value) -- обход метаметода
end
})

proxy.x = 10 -- Вывод: Запись: x = 10

Важно: Для обхода метаметодов используются rawget, rawset, rawlen.

  1. __add, __sub, __mul, __div, __mod, __pow — арифметические метаметоды. Позволяют перегружать арифметические операторы.
local Vec2 = { x = 0, y = 0 }

function Vec2:new(x, y)
local obj = { x = x or 0, y = y or 0 }
setmetatable(obj, self)
self.__index = self
return obj
end

function Vec2:__add(other)
return Vec2:new(self.x + other.x, self.y + other.y)
end

local a = Vec2:new(1, 2)
local b = Vec2:new(3, 4)
local c = a + b
print(c.x, c.y) -- 4 6

Таким образом, Lua поддерживает операторную перегрузку, аналогично C++ или Python.

  1. __call — вызов таблицы как функции. Позволяет использовать таблицу как вызываемый объект.
local Counter = { count = 0 }
function Counter:__call()
self.count = self.count + 1
return self.count
end

setmetatable(Counter, Counter)

print(Counter()) -- 1
print(Counter()) -- 2

Часто применяется для создания фабрик, синглтонов или DSL.

  1. __tostring — строковое представление. Определяет, как таблица конвертируется в строку (например, при print).
function Vec2:__tostring()
return string.format("Vec2(%g, %g)", self.x, self.y)
end

print(a) -- Vec2(1, 2)

Без __tostring print(t) выводит что-то вроде table: 0x....

  1. __eq, __lt, __le — сравнение таблиц. По умолчанию таблицы сравниваются по ссылке. Эти метаметоды позволяют задать логическое равенство или порядок.
function Vec2:__eq(other)
return self.x == other.x and self.y == other.y
end

setmetatable(Vec2, { __eq = Vec2.__eq })

local v1 = Vec2:new(1, 2)
local v2 = Vec2:new(1, 2)
print(v1 == v2) -- true (если метаметод установлен корректно)

Ограничение: __eq работает только при явном сравнении двух таблиц с одним и тем же метаметодом.

Lua не имеет встроенных классов, но предоставляет все средства для эмуляции объектно-ориентированного программирования на основе таблиц и делегирования.

«Класс» в Lua — это таблица, выступающая как прототип и хранилище методов.

Person = {}
Person.__index = Person

function Person:new(name, age)
local instance = setmetatable({}, self)
instance.name = name
instance.age = age
return instance
end

function Person:greet()
print("Привет, меня зовут " .. self.name)
end

Наследование реализуется путём установки родительского прототипа как метатаблицы дочернего класса.

Student = Person:new()  -- Student наследует от Person
Student.__index = Student

function Student:new(name, age, grade)
local instance = Person:new(name, age)
setmetatable(instance, self)
instance.grade = grade
return instance
end

local s = Student:new("Анна", 20, "A")
s:greet() -- Привет, меня зовут Анна

Цепочка поиска методов:

  • Поле ищется в самой таблице.
  • Если не найдено — в __index метатаблицы.
  • Рекурсивно, пока не будет найдено или не завершится цепочка.

Это прототипное наследование, аналогичное JavaScript.

Хотя Lua не поддерживает полиморфизм в классическом смысле (перегрузка функций по сигнатурам), метаметоды обеспечивают поведенческий полиморфизм:

  • Один и тот же оператор (+, #, () и т.д.) вызывает разные функции в зависимости от типа операндов.
  • Это ад-хок полиморфизм, близкий к тому, что реализован в Python (__add__) или Ruby (+).
-- Сложение векторов
mt_vec.__add = add_vectors

-- Сложение матриц
mt_mat.__add = add_matrices

-- Оператор + работает полиморфно
a + b -- вызовет нужную реализацию в зависимости от типов

Lua предлагает минималистичный, но мощный механизм, позволяющий строить сложные абстракции поверх простых примитивов. Однако такая гибкость требует дисциплины: без соглашений код может стать трудным для понимания. Метатаблицы и метаметоды — это фундаментальная абстракция, превращающая Lua из простого скриптового языка в мощную платформу для создания доменно-ориентированных языков (DSL), игровых движков, конфигурационных систем и сложных ООП-архитектур.